/**
 *  Copyright (C) 2008-2013  Telosys project org. ( http://www.telosys.org/ )
 *
 *  Licensed under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *          http://www.gnu.org/licenses/lgpl.html
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.telosys.persistence.mapping;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.LinkedList;

import org.telosys.persistence.TelosysPersistenceException;
import org.telosys.persistence.annotation.BooleanStoredAsInteger;
import org.telosys.persistence.annotation.BooleanStoredAsString;
import org.telosys.persistence.annotation.LinkToMany;
import org.telosys.persistence.annotation.LinkToOne;
import org.telosys.persistence.annotation.LinksToMany;
import org.telosys.persistence.annotation.LinksToOne;
import org.telosys.util.introspection.BeanIntrospector;

public class EntityIntrospector 
{
	private static void log(String msg) {
		System.out.println(msg);
	}

	/**
	 * Returns the entity mapping defined by annotations in the entity class (or null if no annotations)
	 * @param entityClass
	 * @return the mapping or null
	 */
	public static <T> EntityMapping<T> getEntityMapping(Class<T> entityClass) 
	{
		if ( null == entityClass ) {
			throw new IllegalArgumentException("Entity class is null");
		}
		
		EntityMapping<T> entityMapping = getEntityMappingFromAnnotations(entityClass); 

		return entityMapping ;
	}

	private final static String[] TABLE_ANNOTATION = 
		{ "javax.persistence.Table",  "org.telosys.persistence.annotation.Table"  };
	
	private final static String[] COLUMN_ANNOTATION = 
		{ "javax.persistence.Column", "org.telosys.persistence.annotation.Column" };
	
	private final static String[] ID_ANNOTATION              = 
		{ "javax.persistence.Id",     "org.telosys.persistence.annotation.Id"     };
	
	private final static String[] GENERATED_VALUE_ANNOTATION = 
		{ "javax.persistence.GeneratedValue",     "org.telosys.persistence.annotation.GeneratedValue"     };
	private final static String[] GENERATION_TYPE_ENUM = 
		{ "javax.persistence.GenerationType",     "org.telosys.persistence.annotation.GenerationType"     };
	
	private final static String[] TEMPORAL_ANNOTATION = 
		{ "javax.persistence.Temporal",           "org.telosys.persistence.annotation.Temporal"         };
	private final static String[] TEMPORAL_TYPE_ENUM = 
		{ "javax.persistence.TemporalType",       "org.telosys.persistence.annotation.TemporalType"     };
	
	private final static String[] LOB_ANNOTATION  = 
		{ "javax.persistence.Lob",                "org.telosys.persistence.annotation.Lob"     };

//	private final static String[] BOOLEAN_STORAGE_ANNOTATION = 
//		{ "org.telosys.persistence.annotation.BooleanStorage"     };
	
	private static AttributeMapping getFieldMappingFromAnnotations( Field field ) 
	{
		Class<?>  fieldClass         = field.getType();
		String    fieldCanonicalName = fieldClass.getCanonicalName() ;

		String  fieldName  = null ;
		String  columnName = null ;
		boolean primaryKey = false ;
		boolean autoIncr   = false ;
		int     keyPart    = 0 ; 

		DateType              dateType = null ;
		Object                booleanStorage = null;
		ByteArrayStorageType  byteArrayStorageType = null;
		
		fieldName  = field.getName();
		Annotation[] annotations = field.getAnnotations();
		for ( Annotation annotation : annotations ) 
		{
			//---  @Column(name="FIRST_NAME", length=40)
			if ( annotationNameEquals(annotation, COLUMN_ANNOTATION ) )
			{
				log("@Column found" );
				columnName   = BeanIntrospector.invokeGetter(annotation, "name",   String.class) ;
				log("@Column name = " + columnName );
			}
			
			//---  @Id
			else if ( annotationNameEquals(annotation, ID_ANNOTATION ) )
			{
				//--- Flag "id" ( no other values )
				log("@Id found" );
				primaryKey = true ;
				
				//--- Try to get the "keyPart" (only for Telosys @Id annotation)
				keyPart = 0 ; // By default
				if ( annotation instanceof org.telosys.persistence.annotation.Id ) {
					org.telosys.persistence.annotation.Id idAnnotation = (org.telosys.persistence.annotation.Id) annotation ;
					keyPart = idAnnotation.keyPart(); // ZERO by default in annotation definition
				}
				log("@Id keyPart = " + keyPart );
				
			}
			
			//---  @GeneratedValue(strategy=GenerationType.AUTO)
			else if ( annotationNameEquals(annotation, GENERATED_VALUE_ANNOTATION ) )
			{
				log("@GeneratedValue found" );
				Object strategy  = BeanIntrospector.invokeGetter(annotation, "strategy", Object.class) ;
				log("@GeneratedValue 'strategy' class = " + strategy.getClass().getCanonicalName() );
				String generator  = BeanIntrospector.invokeGetter(annotation, "generator", String.class) ;
				log("@GeneratedValue 'strategy' class = " + strategy.getClass().getCanonicalName() );
				log("@GeneratedValue 'generator' = " + generator );

				Class<?> strategyClass = strategy.getClass() ;
				if ( strategyClass.isEnum() ) {
					if ( classNameEquals(strategyClass, GENERATION_TYPE_ENUM ) ) {
						Object[] constants = strategyClass.getEnumConstants() ;
						for ( Object c : constants ) {
							if ( strategy.equals(c) ) {
								log("@GeneratedValue 'strategy' = " + c );
								if ( "AUTO".equals( c.toString() ) ) {
									log("@GeneratedValue 'strategy' is AUTO" );
									throw new TelosysPersistenceException("@GeneratedValue : strategy 'AUTO' not supported");
								}
								else if ( "IDENTITY".equals( c.toString() ) ) {
									log("@GeneratedValue 'strategy' is IDENTITY" );
									autoIncr = true ;
								}
								else if ( "SEQUENCE".equals( c.toString() ) ) {
									log("@GeneratedValue 'strategy' is SEQUENCE" );
									throw new TelosysPersistenceException("@GeneratedValue : strategy 'SEQUENCE' not supported");
								}
								else if ( "TABLE".equals( c.toString() ) ) {
									log("@GeneratedValue 'strategy' is TABLE" );
									throw new TelosysPersistenceException("@GeneratedValue : strategy 'TABLE' not supported");
								}
							}
						}
					}
				}
			}

			//--- @Temporal(TemporalType.TIMESTAMP)
			else if ( annotationNameEquals(annotation, TEMPORAL_ANNOTATION ) )
			{
				if ( "java.util.Date".equals( fieldCanonicalName ) ) 
				{
					log("@Temporal found : " );
					Object temporalValue  = BeanIntrospector.invokeGetter(annotation, "value", Object.class) ;
					log("@Temporal 'value' class = " + temporalValue.getClass().getCanonicalName() );

					Object[] constants = getEnumConstants(temporalValue, TEMPORAL_TYPE_ENUM) ;
					for ( Object c : constants ) {
						if ( temporalValue.equals(c) ) {
							log("@Temporal 'value' = " + c );
							if ( "DATE".equals( c.toString() ) ) {
								log("@Temporal 'value' = DATE => date only" );
								dateType = DateType.dateOnly ;
							}
							else if ( "TIME".equals( c.toString() ) ) {
								log("@Temporal 'value' = TIME => time only" );
								dateType = DateType.timeOnly ;
							}
							else if ( "TIMESTAMP".equals( c.toString() ) ) {
								log("@Temporal 'value' = TIMESTAMP => date and time" );
								dateType = DateType.dateAndTime ;
							}
						}
					}
				}
			}
			
			//--- @Lob 
			else if ( annotationNameEquals(annotation, LOB_ANNOTATION ) )
			{
				if ( byte[].class.getCanonicalName().equals( fieldCanonicalName ) ) 
				{
					byteArrayStorageType = ByteArrayStorageType.BLOB ;
				}
				else if ( char[].class.getCanonicalName().equals( fieldCanonicalName ) ) 
				{
					// Nothing to do : char[] is always considered as a CLOB
				}
			}
			
			//========== Specific Telosys annotations
			
			//--- @BooleanStoredAsInteger(trueValue=1, falseValue=0)
			else if ( annotation instanceof BooleanStoredAsInteger ) 
			{
				if ( boolean.class.getCanonicalName().equals( fieldCanonicalName ) || Boolean.class.getCanonicalName().equals( fieldCanonicalName ) ) 
				{
					BooleanStoredAsInteger a = (BooleanStoredAsInteger) annotation ;
					booleanStorage = new BooleanMappedToInt(a.trueValue(), a.falseValue() ) ;
				}
			}

			//--- @BooleanStoredAsString(trueValue="T", falseValue="F")
			else if ( annotation instanceof BooleanStoredAsString ) 
			{
				if ( boolean.class.getCanonicalName().equals( fieldCanonicalName ) || Boolean.class.getCanonicalName().equals( fieldCanonicalName ) ) 
				{
					BooleanStoredAsString  a = (BooleanStoredAsString) annotation ;
					booleanStorage = new BooleanMappedToString( a.trueValue(), a.falseValue() ) ;
				}
			}
			
		}
		
		if ( null == columnName ) {
			return null ; // No column name => no mapping 
		}
		
	
		AttributeMapping attributeMapping ;
		if ( dateType != null ) {
			attributeMapping = new AttributeMapping(fieldName, columnName, primaryKey, autoIncr, keyPart, dateType ) ;
		}
		else if ( booleanStorage != null ) {
			attributeMapping = new AttributeMapping(fieldName, columnName, primaryKey, autoIncr, keyPart, booleanStorage ) ;
		}
		else if ( byteArrayStorageType != null ) {
			attributeMapping = new AttributeMapping(fieldName, columnName, primaryKey, autoIncr, keyPart, byteArrayStorageType ) ;
		}
		else {
			attributeMapping = new AttributeMapping(fieldName, columnName, primaryKey, autoIncr, keyPart ) ;
		}
		
		log(" Field : " + fieldName + " --> " + columnName + " (PK ? " + primaryKey + ") " + autoIncr );
		return attributeMapping ;
	}

	
	private static boolean equals(String s, String[] values) 
	{
		for ( String value : values ) {
			if ( s.equals(value) ) {
				return true ;
			}
		}
		return false ;
	}
	
	private static boolean classNameEquals(Class<?> clazz, String[] values) 
	{
		String className = clazz.getCanonicalName();
		if ( equals ( className, values ) )
		{
			return true ; 
		}
		return false ;
	}
	
	//private static boolean annotationNameEndsWith(Annotation annotation, String end) 
	private static boolean annotationNameEquals(Annotation annotation, String[] values) 
	{
		Class<? extends Annotation> cl = annotation.getClass();
		String className = cl.getCanonicalName();
		//log("Annotation class = " + className );
		
		//if ( className.endsWith(end) )  // Never happens ( annotation is an interface implementation )
		if ( equals ( className, values ) ) // Never happens ( annotation is an interface implementation )
		{
			return true ; 
		}
		
		Class<?>[] interfaces = cl.getInterfaces(); // An annotation implements only one interface 
		for ( Class<?> i : interfaces ) {
			String interfaceName = i.getCanonicalName() ;
			//log( className + " implements " + interfaceName );			
			//if ( interfaceName.endsWith(end) )
			if ( equals ( interfaceName, values ) )
			{
				return true ; 
			}
		}
		
		return false ;
	}
	
	private static <T> EntityMapping<T> getEntityMappingFromAnnotations(Class<T> entityClass) 
	{
		String tableName   = null ;
		String tableSchema = null ;
		//--- Links mapping
		LinkedList<LinkMapping> linksToOneMapping  = new LinkedList<LinkMapping>();
		LinkedList<LinkMapping> linksToManyMapping = new LinkedList<LinkMapping>();
		
		//----- ENTITY CLASS ANNOTATIONS
		Annotation[] annotations = entityClass.getAnnotations();
		for ( Annotation a : annotations ) 
		{
			if ( annotationNameEquals(a, TABLE_ANNOTATION ) )	
			{
				//----- ANNOTATIONS FOR CLASS 
				log("@Table found : " );
				tableName   = BeanIntrospector.invokeGetter(a, "name",   String.class) ;
				log("@Table name = " + tableName );
				tableSchema = BeanIntrospector.invokeGetter(a, "schema", String.class) ;
				log("@Table schema = " + tableSchema );

			}
			else {
				//--- Telosys LPA annotations for LINKS management
				if ( a instanceof LinksToOne ) {
					//--- LINKS "TO ONE"
					LinksToOne linksToOne = (LinksToOne) a ;
					LinkToOne[] linkToOneArray = linksToOne.value();
					for ( LinkToOne linkToOne : linkToOneArray ) {
						String   name     = linkToOne.name() ;
						Class<?> type     = linkToOne.type() ;
						String   keyParts = linkToOne.keyParts();
						
						log("@LinkToOne : " + name + ", " + type + ", " + keyParts );
						linksToOneMapping.add( new LinkMapping(type, name, '1', keyParts) );
					}
				}
				if ( a instanceof LinksToMany ) {
					//--- LINKS "TO MANY"
					LinksToMany linksToMany = (LinksToMany) a ;
					LinkToMany[] linkToManyArray = linksToMany.value();
					for ( LinkToMany linkToMany : linkToManyArray ) {
						String   name      = linkToMany.name() ;
						Class<?> type      = linkToMany.type() ;
						String   keyParts  = linkToMany.keyParts();
						
						log("@LinkToMany : " + name + ", " + type + ", " + keyParts  );
						linksToManyMapping.add( new LinkMapping(type, name, 'N', keyParts) );
					}
				}
			}
			
		}
		
		if ( tableName != null ) {
			//--- This bean is mapped to a table => get fields annotations
			LinkedList<AttributeMapping> attributesMapping = new LinkedList<AttributeMapping>();
			
			for ( Field field : entityClass.getDeclaredFields() ) {
				log(". " + field.getName() + " : type " + field.getType().getCanonicalName() );
				AttributeMapping am = getFieldMappingFromAnnotations( field );
				if ( am != null ) {
					log("===> " + am);
					attributesMapping.add(am);
				}
			}

			//--- BUILD MAPPING DESCRIPTION
			AttributeMapping[] attributesMappingArray = attributesMapping.toArray(new AttributeMapping[0]);
			
			EntityMapping<T> entityMapping = new EntityMapping<T>(entityClass, tableName, tableSchema, 
					attributesMappingArray, linksToOneMapping, linksToManyMapping ) ;
			
			return entityMapping ;
		}
		
		return null ;
	}
	
	private static Object[] getEnumConstants(Object enumValue, String[] enumTypes) 
	{
		Class<?> enumValueClass = enumValue.getClass() ;
		if ( enumValueClass.isEnum() ) {
			if ( classNameEquals(enumValueClass, enumTypes ) ) {
				Object[] enumConstants = enumValueClass.getEnumConstants() ;
				return enumConstants ;
			}
			else {
				throw new TelosysPersistenceException("Enum value is not an instance of the expected class");
			}
		}
		else {
			throw new TelosysPersistenceException("Enum value argument is not an enumeration");
		}
	}
	
}
